from django.conf                import settings
from django.contrib.auth.models import User
from django.contrib.gis.db      import models


class QBEManager(models.GeoManager):
    """A custom manager that adds query-by-example functionality (inspired
    by Hibernate)"""
    def similar_to(self, template):
        """Find all objects that have fields matching those in the template
        object.  Fields that are set to None are ignored."""
        vals = {}
        for f in template._meta.get_all_field_names():
            _,_,direct,m2m = template._meta.get_field_by_name(f)
            if not direct or m2m or f == 'id':
                continue
            val = getattr(template, f, None)
            if val is not None:
                vals[f] = val

        return super(QBEManager, self).get_query_set().filter(**vals)

class ConnieRun(models.Model):
    """
    Represents the configuration parameters for a single connie run,
    as access via the web interface (more sophisticated settings are
    available via the raw interface to the connie2 process of course).
    """
    isSource         = models.BooleanField(default=True)

    # There's aren't strictly speaking dates as we aren't concerned
    # with the year; maintaining them as dates for now though in case
    # things change in the future.
    spawningFromDate = models.DateField()
    spawningToDate   = models.DateField()

    years            = models.ManyToManyField('YearForAveraging')

    regions          = models.MultiPolygonField()

    boundingBox      = models.PolygonField()

    # This is automatically populated when it is created, and used in
    # ordering etc to present to the user:
    creationTime     = models.DateTimeField(auto_now_add=True)

    user             = models.ForeignKey(User, blank=True, null=True)

    # The velocity fields info.
    velocityFields   = models.ForeignKey('VelocityFields', default=1)

    # Required for spatial queries:
    objects          = QBEManager()

    # Calculated property:
    def duration(self):
        return sum(phase.phaseLongevity for phase in self.larvalPhases.all())

    def calcBounds(self):
        """This could effectively be a calculated property, except that we
        sometimes want to over-ride the defaults, so this becomes a utility
        method which is often used to calculate boundingBox."""
        from django.contrib.gis.geos import Polygon
        # user selection extent:
        uminx, uminy, umaxx, umaxy = self.regions.extent
        # UV bounds extent:
        fminx, fminy, fmaxx, fmaxy = self.velocityFields.bounds.extent
        # "padding" in degrees -- simple heuristic based on duration for now:
        offset = 10 if self.duration() < 30 else 20
        return Polygon.from_bbox((max(uminx-offset, fminx),
                                  max(uminy-offset, fminy),
                                  min(umaxx+offset, fmaxx),
                                  min(umaxy+offset, fmaxy)))

    def imgSize(self):
        """We are emulating matplotlib's figaspect calculation here, since
        openlayers Image layer requires the image size in pixels.  Aspect
        ratio can be calculated from the bounding box, then we emulate the
        calculations.  Figaspect uses 6" for the height then adjusts the
        width proportionally, with some range checks as well.  We simply do
        the same, and make the same assumption that savefig appears to do of
        100 pixels per inch.  At worst I think it confuses the zooming
        calculations a bit.  Returns a tuple of width,height in pixels."""
        minx, miny, maxx, maxy = self.boundingBox.extent
        aspect = (maxy-miny)/(maxx-minx)
        return 600/aspect, 600

    def __unicode__(self):
        vals = {}
        days = self.duration()
        vals['ld'] = days
        vals['plural'] = 's' if days > 1 else ''
        vals['from'] = self.spawningFromDate.strftime('%d/%m')
        vals['to']   = self.spawningToDate.strftime('%d/%m')
        years = [year.year for year in self.years.all()]
        vals['years'] = str(years[0] if len(years) == 1 else years)
        vals['level'] = self.larvalPhases.get(phaseNumber=1).levels.all()[0].verticalLevel.depth
        vals['created'] = self.creationTime.isoformat(' ')
        return u'%(ld)d day%(plural)s release, %(from)s-%(to)s %(years)s @ %(level)dm (created %(created)s)' % vals


class LarvalPhase(models.Model):
    """
    A single dispersal phase, for example the first week in the life
    of a critter after spawning.  Consists of one or more vertical
    levels (that they may inhabit during different periods of the day,
    for example), and associated information about longevity of the
    phase, movement behaviour in this phase, etc.  Every ConnieRun has
    at least one LarvalPhase associated with it.
    """
    # Refer to the configuration for which this phase applies; note we
    # will usually want to access this the other way, so we set the
    # related_name here so we can do run.larvalPhases.all()...
    connieRun           = models.ForeignKey(ConnieRun, related_name='larvalPhases')
    phaseNumber         = models.IntegerField(default=1) # used to order the phases
    phaseLongevity      = models.IntegerField(verbose_name='Dispersal Time (days)') # in days.
    # horizontal propulsion options:
    northwardPropulsion = models.FloatField(default=0, verbose_name='North (m/s)') # m/s
    eastwardPropulsion  = models.FloatField(default=0, verbose_name='East (m/s)') # m/s
    randomPropulsion    = models.FloatField(default=0, verbose_name='Random (m/s)') # m/s
    windspeedPropulsion = models.FloatField(default=0, verbose_name='Wind (%)') # %

class LarvalPhaseLevel(models.Model):
    """
    Represents a single level within a single dispersal phase; that
    is, the vertical level and the associated time period (random,
    night, etc).  Every LarvalPhase has at least one LarvalPhaseLevel
    associated with it; if there is only a single level, then the
    'period' attribute is ignored, and the configuration is treated as
    a basic one with no behaviours.
    """
    TIMEOFDAY_CHOICES = ((1, 'Day'),
                         (2, 'Night'),
                         # (3, 'Flood-tide'),
                         # (4, 'Ebb-tide'),
                         # (5, 'Random'),
                         )
    larvalPhase = models.ForeignKey(LarvalPhase, related_name='levels')
    verticalLevel = models.ForeignKey('VelocityFieldsDepth', verbose_name='depth (m)')
    period = models.IntegerField(choices=TIMEOFDAY_CHOICES, default=5, verbose_name='during')

class YearForAveraging(models.Model):
    """
    A single year for which statistics are gathered.  This is only
    required because a run might gather statistics for multiple years,
    and we can't store a list in the database.  Note that a many-many
    relationship is involved, and the solitary field is unique, so
    code should use get_or_create().
    """
    year             = models.PositiveSmallIntegerField(unique=True)

    def __unicode__(self): return unicode(self.year)


# Most of this is actually configured in the settings for now, but we
# do need to be able to store the uv fields that are used for a run,
# so we'll add all the info required.
class VelocityFields(models.Model):
    """
    Describes the parameters of a set of velocity fields (cell size,
    boundaries, name, etc).
    """
    key        = models.CharField(max_length=20, unique=True) # key as used by connie
    name       = models.CharField(max_length=50, unique=True) # human-readable name
    cellWidth  = models.FloatField()
    cellHeight = models.FloatField()
    bounds     = models.PolygonField()
    minDate    = models.DateField()
    maxDate    = models.DateField()
    depths     = models.ManyToManyField('VelocityFieldsDepth')

    # Want to be able to disable fields form time to time
    enabled    = models.BooleanField(default=True)

    # Required for spatial queries:
    objects    = models.GeoManager()
    def __unicode__(self):
        return unicode(self.name)

class VelocityFieldsDepth(models.Model):
    """
    Each entry describes a single depth offered by one or more sets of
    velocity fields.
    """
    depth = models.IntegerField(unique=True)
    def __unicode__(self): return unicode(self.depth)

    class Meta:
        ordering = ('depth',)
